package cn.flying.base.security.configuration;

import javax.annotation.Resource;
import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

import cn.flying.base.security.handler.CustomAccessDeniedHandler;
import cn.flying.base.security.handler.CustomLogoutSuccessHandler;
import cn.flying.base.security.handler.LoginFailureHandler;
import cn.flying.base.security.handler.LoginSuccessHandler;
import cn.flying.base.security.handler.UnauthorizedEntryPointHandler;
import cn.flying.base.security.session.SessionExpiredStrategy;
import cn.flying.base.security.session.SessionInvalidStrategy;

/**
 * @description: security 配置类
 * @author: lvyong
 * @date: 2022年11月07日 20:36
 * @version: 1.0
 */
@EnableRedisHttpSession
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
//@EnableGlobalAuthentication
@EnableWebSecurity(debug = false)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Resource
    private LoginUserDetailService loginUserDetailService;
    @Resource
    private LoginSuccessHandler loginSuccessHandler;
    @Resource
    private LoginFailureHandler loginFailureHandler;
    @Resource
    private UnauthorizedEntryPointHandler unauthorizedEntryPointHandler;
    @Resource
    private CustomAccessDeniedHandler customAccessDeniedHandler;
    @Resource
    private CustomLogoutSuccessHandler customLogoutSuccessHandler;
    @Resource
    private SessionExpiredStrategy sessionExpiredStrategy;
    @Resource
    private SessionInvalidStrategy sessionInvalidStrategy;
    @Resource
    private CustomWebAuthenticationDetailsSource customWebAuthenticationDetailsSource;
    @Resource
    private DataSource dataSource;

    /**
     * 定制基于 HTTP 请求的用户访问控制
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //开启表单验证
        http.formLogin()
                .loginPage("/login")
                //登陆请求的url地址，自定义即可
                .loginProcessingUrl("/doLogin")
                //设置登录验证成功认证成功处理器
                .successHandler(loginSuccessHandler)
                //设置登录验证成功认证失败处理器
                .failureHandler(loginFailureHandler)
                //此处的登录认证成功/失败跳转和上面的successHandler/failureHandler会冲突，只保留一种
//                .defaultSuccessUrl("/")
//                .failureUrl("/login")
                //指定authenticationDetailsSource
                .authenticationDetailsSource(customWebAuthenticationDetailsSource);

        //请求访问控制，设置需要拦截的请求
        http.authorizeRequests()
                //放行请求，这里的所有请求必须是post提交
                .antMatchers("/doLogin", "/doRegister").permitAll()
                //其他的请求都需要进行身份认证
                .anyRequest().authenticated()
                //对于所有的请求都需要采用该方法校验是否有权限
//                .anyRequest().access("@rbacService.hasPermission(request, authentication)")
                .and()
                .exceptionHandling()
                //匿名无权限访问
                .authenticationEntryPoint(unauthorizedEntryPointHandler)
                //认证用户无权限访问
                .accessDeniedHandler(customAccessDeniedHandler);

        //禁用匿名登录
        http.anonymous().disable();

        //开启 Remember-Me 功能
        http.rememberMe()
                //指定 Remember-Me 功能自动登录过程使用的 UserDetailsService 对象，默认使用 Spring 容器中的 UserDetailsService 对象
                .userDetailsService(loginUserDetailService)
                //指定在登录时“记住我”的 HTTP 参数，默认为 remember-me
                .rememberMeParameter("remember")
                .rememberMeCookieName("remember-me-cookie")
                //记住我”的 Token 中的标识字段，默认是一个随机的 UUID 值
//                .key()
                //“记住我” 的 Token 令牌有效期，单位为秒，即对应的 cookie 的 Max-Age 值，默认时间为 2 周
                .tokenValiditySeconds(2 * 24 * 60 * 60)
                //指定 TokenRepository 对象，用来配置持久化 Token
                .tokenRepository(tokenRepository());

        //session管理配置
        http.sessionManagement()
                //是session的生成策略，前后端分离的时候配置成STATELESS，非前后端分离的需要配置成ALWAYS，这里用ALWAYS，即总是创建HttpSession，默认是IF_REQUIRED
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                //session失效后跳转路径（会话过期）
//                .invalidSessionUrl("/login")
                //session失效策略，和上面invalidSessionUrl二选一
                .invalidSessionStrategy(sessionInvalidStrategy)
                //session保护  migrateSession默认（防御会话固定攻击）
                .sessionFixation().migrateSession()
                //限制最大登录用户（客户端）数 （会话并发控制）
                .maximumSessions(1)
                //true 表示已经登录了就不能在登录了，false表示允许再次登录但是会将原来登录的用户踢下线，使用这个需要配置一个Bean（HttpSessionEventPublisher）
                .maxSessionsPreventsLogin(false)
//                .expiredUrl("")
                //session到期策略
                .expiredSessionStrategy(sessionExpiredStrategy);

        //登出配置
        http.logout()
                .logoutUrl("/doLogout")
                .logoutSuccessHandler(customLogoutSuccessHandler)
                // 指定用户注销登录时删除的 Cookie
                .deleteCookies("JSESSIONID")
                //用户注销登录时是否立即清除用户的 Session，默认为 true
                .invalidateHttpSession(true)
                // 用户注销登录时是否立即清除用户认证信息 Authentication，默认为 true
                .clearAuthentication(true);

        //开启跨域配置，禁用csrf防御机制,是禁用使用跨站请求,可解决POST请求403问题
        http.cors().and().csrf().disable().headers().frameOptions().disable();
    }

    /**
     * 配置忽略资源
     * 一般是配置系统静态资源用，配置的请求根本不会进入 spring security 的过滤器链，直接放行
     * .antMatchers("/").permitAll() 是会进入 spring security 的过滤器链的，这是 2 者的主要区别
     * /login（登录）、/captcha（获取图片验证码）、/forget（忘记密码）、/register（注册）、/error（错误页面）
     *
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(
                "/static/**",
                "/css/**",
                "/font/**",
                "/images/**",
                "/js/**",
                "/layui/**",
                "/lib/**",
                "/style/**",
                "/ztree/**",
                "/modules/**",
                "/adminui/**",
                "/config.js",
                "/index.js",
                "/favicon.ico",
                "/error",
                "/captcha",
                "/forget",
                "/login",
                "/register",
                "/auth/code",
                "/auth/valid/code",
                "/resetpass",
                "/druid/**"

        );
    }

    /**
     * 定制用户认证管理器来实现用户认证，测试可以采用内存inMemoryAuthentication
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 指定UserDetailService和加密器，不再使用内存方式存储用户认证信息，而是动态从数据库中获取
        auth.userDetailsService(loginUserDetailService).passwordEncoder(passwordEncoder());
    }

    /**
     * 加密处理对象
     * 使用 BCryptPasswordEncoder 密码编码器，该编码器会将随机产生的 salt 混入最终生成的密文中
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 初始化一个自定义的校验provider
     *
     * @return
     */
    @Bean
    public CustomAuthenticationProvider customAuthenticationProvider() {
        CustomAuthenticationProvider provider = new CustomAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder());
        provider.setUserDetailsService(loginUserDetailService);
        return provider;
    }

    /**
     * 认证管理器添加自定义provider
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return new ProviderManager(customAuthenticationProvider());
    }

    /**
     * 配置 Session 的监听器（如果使用并发 Sessoion 控制，一般都需要配置）
     * 解决 Session 失效后, SessionRegistry 中 SessionInformation 没有同步失效问题
     *
     * logout 退出后需要发布一个事件，通过将HttpSessionEventPublisher注入到ioc容器中，实现session的清除，
     * 如果配置http.maxSession.timeout，注入bean才会生效
     *
     * @return
     */
    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {

        return new HttpSessionEventPublisher();
    }

    /**
     * 注入SessionRegistry到ioc容器，便于业务处理使用，如需要获取session数量
     *
     * @return
     */
    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    /**
     * 配置 JdbcTokenRepositoryImpl，用于 Remember-Me 的持久化 Token
     */
    @Bean
    public JdbcTokenRepositoryImpl tokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        // 配置数据源
        jdbcTokenRepository.setDataSource(dataSource);
        // 第一次启动的时候可以使用以下语句自动建表（可以不用这句话，自己手动建表，源码中有语句的）
//        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }
}
